Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.IO

Namespace C4F.DevKit.Calendar.Parser
	''' <summary>
	''' A class used to create and add to iCal files.
	''' </summary>
	Public Class Writer
		#Region "Private Members"

		Private _streamWriter As StreamWriter

		#End Region

		#Region "Public Properties"

		''' <summary>
		''' The path of the file to write the calendar to.
		''' </summary>
		Public Property FilePath() As String
			Set(ByVal value As String)
				_streamWriter = New StreamWriter(New FileStream(value, FileMode.OpenOrCreate))
			End Set

			Get
				Return (CType(_streamWriter.BaseStream, FileStream)).Name
			End Get
		End Property

		#End Region

		#Region "Public Constructors"

		''' <summary>
		''' Creates a new calendar writer.
		''' </summary>
		''' <param name="filepath">The path for the iCal file to write to.</param>
		Public Sub New(ByVal filepath As String)
			Me.FilePath = filepath
		End Sub

		''' <summary>
		''' Creates a new calendar writer.
		''' </summary>
		''' <param name="fileStream">The FileStream to write the iCal file to.</param>
		Public Sub New(ByVal fileStream As FileStream)
			If (Not fileStream.CanRead) Then
				Throw New ArgumentException("Cannot read from file stream")
			End If

			If (Not fileStream.CanWrite) Then
				Throw New ArgumentException("Cannot write to file stream")
			End If

			_streamWriter = New StreamWriter(fileStream)
		End Sub

		''' <summary>
		''' Creates a new calendar writer.
		''' </summary>
		''' <param name="streamWriter">The StreamWriter that is attached to the file to write the iCal file to.</param>
		Public Sub New(ByVal streamWriter As StreamWriter)
			If (Not streamWriter.BaseStream.CanRead) Then
				Throw New ArgumentException("Cannot read from base stream of StreamWriter")
			End If

			_streamWriter = streamWriter
		End Sub

		#End Region

		#Region "Public Methods"

		''' <summary>
		''' Creates a new iCal file.
		''' </summary>
		''' <param name="appts">The appointments to write to the calendar.</param>
		Public Sub CreateCalendar(ByVal appts As IEnumerable(Of Appointment))
			Validate(appts)
			Dim filepath As String = Me.FilePath
			_streamWriter.Close()
			File.Delete(filepath)
			Me.FilePath = filepath
			_streamWriter.WriteLine(CalendarConstants.BEGIN_CALENDAR)
			_streamWriter.WriteLine(CalendarConstants.CALENDAR_METADATA)

			For Each appt As Appointment In appts
				Write(appt)
			Next appt

			WriteCalendarEnd()
		End Sub

		''' <summary>
		''' Adds a new appointment to an existing calendar.
		''' </summary>
		''' <param name="appt">The appointment to add.</param>
		Public Sub AddToCalendar(ByVal appt As Appointment)
			Dim appts As List(Of Appointment) = New List(Of Appointment)()
			appts.Add(appt)
			AddToCalendar(appts)
		End Sub

		''' <summary>
		''' Adds new appointments to an exisiting calendar.
		''' </summary>
		''' <param name="appts">The appointments to add.</param>
		Public Sub AddToCalendar(ByVal appts As IEnumerable(Of Appointment))
			Validate(appts)
			_streamWriter.BaseStream.Seek(0, SeekOrigin.Begin)
			Dim streamReader As StreamReader = New StreamReader(_streamWriter.BaseStream)
			Dim currentLine As String = streamReader.ReadLine()

			Do While Not currentLine.StartsWith(CalendarConstants.END_CALENDAR)
				currentLine = streamReader.ReadLine()
			Loop

			_streamWriter.BaseStream.Seek(-1 * (currentLine.Length + _streamWriter.NewLine.Length), SeekOrigin.Current)

			For Each appt As Appointment In appts
				Write(appt)
			Next appt

			WriteCalendarEnd()
		End Sub

		''' <summary>
		''' Updates the information in an existing appointment.
		''' </summary>
		''' <param name="appt">The new version of the appointment to put in the calendar. This appointment
		''' will replace the appointment in the Calendar with the same UID.</param>
		Public Sub EditAppointment(ByVal appt As Appointment)
			Validate(appt)
			Dim appts As IEnumerable(Of Appointment) = Reader.ReadCalendar(CType(_streamWriter.BaseStream, FileStream))
			Dim apptsToWrite As List(Of Appointment) = New List(Of Appointment)()
			For Each currAppt As Appointment In appts
				If (Not currAppt.UID.Equals(appt.UID)) Then
					apptsToWrite.Add(currAppt)
				End If
			Next currAppt

			apptsToWrite.Add(appt)
			Dim filePath As String = (CType(_streamWriter.BaseStream, FileStream)).Name
			File.Delete(filePath)
			_streamWriter = New StreamWriter(filePath)
			CreateCalendar(apptsToWrite)
		End Sub

		''' <summary>
		''' Writes the iCal data out to disk and closes the file.
		''' </summary>
		Public Sub Close()
			_streamWriter.Close()
		End Sub

		#End Region

		#Region "Public Static Methods"

		''' <summary>
		''' Creates a new iCal file.
		''' </summary>
		''' <param name="appts">The appointments to write to the calendar.</param>
		''' <param name="filePath">The path of the iCal file to write to.</param>
		Public Shared Sub CreateCalendar(ByVal appts As IEnumerable(Of Appointment), ByVal filePath As String)
			Dim writer As Writer = New Writer(filePath)
			writer.CreateCalendar(appts)
			writer.Close()
		End Sub

		''' <summary>
		''' Adds new appointments to an exisiting calendar.
		''' </summary>
		''' <param name="appts">The appointments to add.</param>
		''' <param name="filePath">The path of the iCal file to write to.</param>
		Public Shared Sub AddToCalendar(ByVal appts As IEnumerable(Of Appointment), ByVal filePath As String)
			Dim writer As Writer= New Writer(filePath)
			writer.AddToCalendar(appts)
			writer.Close()
		End Sub

		''' <summary>
		''' Adds a new appointment to an existing calendar.
		''' </summary>
		''' <param name="appt">The appointment to add.</param>
		''' <param name="filePath">The path of the iCal file to write to.</param>
		Public Shared Sub AddToCalendar(ByVal appt As Appointment, ByVal filePath As String)
			Dim writer As Writer = New Writer(filePath)
			writer.AddToCalendar(appt)
			writer.Close()
		End Sub

		''' <summary>
		''' Updates the information in an existing appointment.
		''' </summary>
		''' <param name="appt">The new version of the appointment to put in the calendar. This appointment
		''' will replace the appointment in the Calendar with the same UID.</param>
		''' <param name="filePath">The calendar file to be edited.</param>
		Public Shared Sub EditAppointment(ByVal appt As Appointment, ByVal filePath As String)
			Dim writer As Writer = New Writer(filePath)
			writer.EditAppointment(appt)
			writer.Close()
		End Sub

		#End Region

		#Region "Private Methods"

		Private Sub Write(ByVal appt As Appointment)
			For Each subAppt As Appointment In appt.Occurrences
				Write(subAppt)
			Next subAppt

			WriteLine(CalendarConstants.BEGIN_APPOINTMENT)

			For Each attendee As Attendee In appt.Attendees
				WriteLine(ConvertToString(attendee))
			Next attendee

			If Not appt.Notes Is Nothing Then
				WriteLine(CalendarConstants.DESCRIPTION & ":" & ParserHelper.InsertEscapeChars(appt.Notes))
			End If

			WriteLine(CalendarConstants.TIMESTAMP & ":" & DateParser.ToUTCString(appt.Timestamp))

			WriteLine(CalendarConstants.START_DATE_TIME & ConvertToString(appt.StartDateTime, appt.IsAllDayEvent))
			WriteLine(CalendarConstants.END_DATE_TIME & ConvertToString(appt.EndDateTime, appt.IsAllDayEvent))

			If (Not String.IsNullOrEmpty(appt.Location)) Then
				WriteLine(CalendarConstants.LOCATION & ":" & ParserHelper.InsertEscapeChars(appt.Location))
			End If

			If Not appt.RecurrenceRule Is Nothing Then
				WriteLine(ConvertToString(appt.RecurrenceRule))
			End If

			If appt.IsPartOfSeries AndAlso appt.SequenceNumber <> 0 Then
				WriteLine(CalendarConstants.OCCURRENCE & ":" & ConvertToString(appt.Occurrence))
			End If

			If appt.IsPartOfSeries Then
				WriteLine(CalendarConstants.SEQUENCE_NUMBER & ":" & appt.SequenceNumber.ToString())
			End If

			WriteLine(CalendarConstants.SUMMARY & ":" & ParserHelper.InsertEscapeChars(appt.Summary))
			WriteLine(CalendarConstants.UID & ":" & appt.UID.ToString().ToUpper())

			If (Not String.IsNullOrEmpty(appt.URL)) Then
				WriteLine(CalendarConstants.URL & ":""" & appt.URL & """")
			End If

			If Not appt.Reminder Is Nothing Then
				WriteLines(ConvertToString(appt.Reminder, appt.StartDateTime))
			End If

			For Each line As String In appt.ExtraProperties
				WriteLine(line)
			Next line

			WriteLine(CalendarConstants.END_APPOINTMENT)
		End Sub

		Private Sub WriteLine(ByVal line As String)
			Dim extraLines As List(Of String) = New List(Of String)()

			If line.Length <= CalendarConstants.LINE_LENGTH_LIMIT Then
				_streamWriter.WriteLine(line)
				Return
			End If

			_streamWriter.WriteLine(line.Substring(0, CalendarConstants.LINE_LENGTH_LIMIT))
			WriteLine(" "c & line.Substring(CalendarConstants.LINE_LENGTH_LIMIT))
		End Sub

		Private Sub WriteLines(ByVal lines As String)
			For Each line As String In lines.Split(New String() {_streamWriter.NewLine}, StringSplitOptions.None)
				WriteLine(line)
			Next line
		End Sub

		Private Sub WriteCalendarEnd()
			_streamWriter.WriteLine(CalendarConstants.END_CALENDAR)
			_streamWriter.Flush()
			_streamWriter.BaseStream.Seek(-1 * (CalendarConstants.END_CALENDAR.Length + _streamWriter.NewLine.Length), SeekOrigin.Current)
		End Sub

		Private Function ConvertToString(ByVal dateTime As DateTime) As String
			Return DateParser.ToUTCString(dateTime)
		End Function

		Private Function ConvertToString(ByVal dateTime As DateTime, ByVal allDay As Boolean) As String
			If allDay Then
				Return ";" & CalendarConstants.DATE_VALUE & ":" & DateParser.ToDateString(dateTime)
			End If

			Return ":" & ConvertToString(dateTime)
		End Function

		Private Function ConvertToString(ByVal reminder As Reminder, ByVal startDateTime As DateTime) As String
			Dim result As String = CalendarConstants.BEGIN_REMINDER & _streamWriter.NewLine
			result = result & CalendarConstants.TRIGGER & ConvertTriggerToString(reminder, startDateTime) + _streamWriter.NewLine
			result = result & CalendarConstants.REMINDER_UID & ":" & reminder.UID.ToString().ToUpper() + _streamWriter.NewLine

			For Each line As String In reminder.ExtraProperties
				result = result & line & _streamWriter.NewLine
			Next line

			result = result & CalendarConstants.END_REMINDER
			Return result
		End Function

		Private Function ConvertToString(ByVal attendee As Attendee) As String
			Dim nameString As String = attendee.Name

			If attendee.Name.IndexOfAny(";:," & ChrW(&H8).ToString() & ChrW(&HA1F).ToString() & ChrW(&H7F).ToString() & """".ToCharArray()) <> -1 Then
				nameString = """" & attendee.Name & """"
			End If

			If String.IsNullOrEmpty(attendee.Name) Then
				Return CalendarConstants.ATTENDEE & ("") & ":" & CalendarConstants.MAILTO & ":" & attendee.EmailAddress
			Else
				Return CalendarConstants.ATTENDEE & (";" & CalendarConstants.COMMON_NAME & "=" & nameString) & ":" & CalendarConstants.MAILTO & ":" & attendee.EmailAddress
			End If
		End Function

		Private Function ConvertToString(ByVal rule As RecurrenceRule) As String
			Dim result As String = CalendarConstants.RECURRENCE_RULE & ":" & CalendarConstants.FREQUENCY & "=" & ConvertToString(rule.Frequency)

			If (Not rule.EndDateTime.Equals(New DateTime())) Then
				result = result & ";" & CalendarConstants.RECURRENCE_END_DATE_TIME & "=" & DateParser.ToUTCString(rule.EndDateTime)
			ElseIf rule.Count <> 0 Then
				result = result & ";" & CalendarConstants.COUNT & "=" & rule.Count.ToString()
			End If

			If rule.Interval <> 0 Then
				result = result & ";" & CalendarConstants.INTERVAL & "=" & rule.Interval.ToString()
			End If

			If rule.MonthWeekdays.Count > 0 Then
				result = result & ";" & CalendarConstants.MONTH_WEEKDAY & "=" & ConvertToString(rule.MonthWeekdays)
			ElseIf rule.MonthDate <> 0 Then
				result = result & ";" & CalendarConstants.MONTH_DATE & "=" & rule.MonthDate.ToString()
			End If

			For Each [property] As String In rule.ExtraProperties
				result = result & ";" & [property]
			Next [property]

			Return result
		End Function

		Private Function ConvertToString(ByVal freq As Frequency) As String
			Return freq.ToString().ToUpper()
		End Function

		Private Function ConvertToString(ByVal monthWeekdays As IEnumerable(Of MonthWeekday)) As String
			Dim weekdayStrings As List(Of String) = New List(Of String)()

			For Each monthWeekday As MonthWeekday In monthWeekdays
				If (monthWeekday.WeekNumber = 0) Then
					weekdayStrings.Add(("") + ConvertToString(monthWeekday.Weekday))
				Else
					weekdayStrings.Add((monthWeekday.WeekNumber.ToString()) + ConvertToString(monthWeekday.Weekday))
				End If
			Next monthWeekday

			Return String.Join(",", weekdayStrings.ToArray())
		End Function

		Private Function ConvertToString(ByVal weekday As Weekday) As String
			Return weekday.ToString().Substring(0, 2).ToUpper()
		End Function

		Private Function ConvertTriggerToString(ByVal reminder As Reminder, ByVal startDateTime As DateTime) As String
			If reminder.IsAbsoluteTrigger Then
				Return ";" & CalendarConstants.DATE_TIME_VALUE & ":" & DateParser.ToUTCString(reminder.OnDateTrigger)
			End If

			Dim outputTimeSpan As TimeSpan = reminder.Trigger.Negate()

			If outputTimeSpan.Days >= 7 Then
				Return CalendarConstants.TRIGGER_PREFIX & (outputTimeSpan.Days / 7).ToString() & "W"c
			ElseIf outputTimeSpan.Days > 0 Then
				Return CalendarConstants.TRIGGER_PREFIX & outputTimeSpan.Days.ToString() & "D"c
			ElseIf outputTimeSpan.Hours > 0 Then
				Return CalendarConstants.TRIGGER_PREFIX & "T"c + outputTimeSpan.Hours.ToString() & "H"c
			ElseIf outputTimeSpan.Minutes > 0 Then
				Return CalendarConstants.TRIGGER_PREFIX & "T"c + outputTimeSpan.Minutes.ToString() & "M"c
			ElseIf outputTimeSpan.Seconds > 0 Then
				Return CalendarConstants.TRIGGER_PREFIX & "T"c + outputTimeSpan.Seconds.ToString() & "S"c
			Else
				Return CalendarConstants.ZERO_LENGTH_TRIGGER
			End If
		End Function

		Private Sub Validate(ByVal appts As IEnumerable(Of Appointment))
			For Each appt As Appointment In appts
				Validate(appt)
			Next appt
		End Sub

		Private Sub Validate(ByVal appt As Appointment)
			If (Not appt.Validate()) Then
				Throw New CalendarException("Invalid appointment. All appointments must have a start time, end time, and summary.")
			End If
		End Sub

		#End Region
	End Class
End Namespace